摘要|Functional Components with React stateless functions and Ramda

原文在这里. 有段时间不看了, 有些忘了,有些地方还有一定的加深.

什么是 React stateless function?

es6的语法

1
2
3
4
5
class List extends React.Component {
render() {
return (<ul>{this.props.children}</ul>);
}
}

简单的 javascripg函数也可以!

1
2
3
4
5
6
7
//Stateless function syntax
const List = function(children) {
return (<ul>{children}</ul>);
};

//ES6 arrow syntax
const List = (children) => (<ul>{children}</ul>);

彻底的模板,没有自己任何的数据,也没有生命周期方法. 纯粹依赖于输入.

首先来定义一个 App Container

目的是最为一个函数接收 app sate 对象

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
import ReactDOM from 'react-dom';

const App = appState => (<div className="container">
<h1>App name</h1>
<p>Some children here...</p>
</div>);
//这里定义了渲染的方法,作为 APP函数的属性,并且是柯理化的, 等待传入 dom 元素
App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node));

export default App;

在纯函数中,state 必须要在外部管理,然后以 props 的形式传递给组件.
下面看看这个解释的例子

Stateless Timer component

简单的 timer 组件只接受 secondsElapsed 参数:

1
2
3
4
5
import React from 'react';

export default ({ secondsElapsed }) => (<div className="well">
Seconds Elapsed: {secondsElapsed}
</div>);

添加到 APP 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';
import ReactDOM from 'react-dom';
import R from 'ramda';
import Timer from './timer';

const App = appState => (<div className="container">
<h1>App name</h1>
//Timer 只从父组件接受 props 作为自己的数据
<Timer secondsElapsed={appState.secondsElapsed} />
</div>);

App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node));

export default App;

最后创建main.js 文件,启动渲染过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import App from './components/app'; //导入容器组件

// 我们已经有了柯理化的方法
//App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node));
//配置好渲染的目标元素
const render = App.render(document.getElementById('app'));
//state 初始值
let appState = {
secondsElapsed: 0
};

//first render 首次渲染
render(appState);
//多次重复渲染
setInterval(() => {
appState.secondsElapsed++;
render(appState);
}, 1000);

对于上面的代码, 变化的是组件的 state, 渲染的目标元素是一直不变的, 所以我们用柯理化配置好一个工厂函数

1
2
//闭包再工作!
const render = App.render(document.getElementById(‘app’));

柯理化返回的函数,等待传入 props

1
(props) => ReactDOM.render(...)

只要 State发生变化,我们需要渲染时,只需要传递 state 就可以了

1
2
3
4
setInterval(() => {
appState.secondsElapsed++;
render(appState);
}, 1000);

每一秒钟, secondsElapsed 属性会递增1, 然后作为参数传递给 render 函数

现在可以实现 Redux 风格的 reduce 函数, reduce式的函数不能突变当前值

1
currentState->newState

使用 Radma 的 Lenses 来实现

1
2
3
4
5
6
7
const secondsElapsedLens = R.lensProp('secondsElapsed');
const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);

setInterval(() => {
appState = incSecondsElapsed(appState);
render(appState);
}, 1000);

首先创建 Lens:

1
const secondsElapsedLens = R.lensProp('secondsElapsed');

lens可以聚焦于给定的属性,不会针对特定的对象, 所以可以重用.

  • View
1
R.view(secondsElapsedLens, { secondsElapsed: 10 });  //=> 10
  • Set
1
R.set(secondsElapsedLens, 11, { secondsElapsed: 10 });  //=> 11
  • 用给定的函数 Set
1
R.over(secondsElapsedLens, R.inc, { secondsElapsed: 10 });  //=> 11

inSecondElapsed reducer 是一个偏应用函数(partial application),
这一行

1
const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);

会返回一个新的函数,一旦用appState 调用, 就会应用 R.inc在 lensed prop secondElapsed 上.

1
appState=incSecondElapsed(appState)

组合 React stateless components

开篇提到,React 组件可以作为函数, 那么可以用 R.compose来 compose 这些函数吗?
当然是可以的

用 React.createClass 是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const TodoList = React.createClass({
render: function() {
const createItem = function(item) {
return (<li key={item.id}>{item.text}</li>);
};

return (<div className="panel panel-default">
<div className="panel-body">
<ul>
{this.props.items.map(createItem)}
</ul>
</div>
</div>);
}
});

现在问题是: TodoList 可以由小的可重用部分 composition 而成吗? 可以的. 可以分为三个更小的组件

  • 容器组件
1
2
3
4
5
const Container = children => (<div className="panel panel-default">
<div className="panel-body">
{children}
</div>
</div>);
  • 列表组件
1
2
3
const List = children => (<ul>
{children}
</ul>);
  • 列表项组件
1
2
3
const ListItem = ({ id, text }) => (<li key={id}>
<span>{text}</span>
</li>);

现在一步一动,看看每一步的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
Container(<h1>Hello World!</h1>);

/**
* <div className="panel panel-default">
* <div className="panel-body">
* <h1>Hello World!</h1>
* </div>
* </div>
*/

Container(List(<li>Hello World!</li>));

/**
* <div className="panel panel-default">
* <div className="panel-body">
* <ul>
* <li>Hello World!</li>
* </ul>
* </div>
* </div>
*/

const TodoItem = {
id: 123,
text: 'Buy milk'
};
Container(List(ListItem(TodoItem)));

/**
* <div className="panel panel-default">
* <div className="panel-body">
* <ul>
* <li>
* <span>Buy milk</span>
* </li>
* </ul>
* </div>
* </div>
*/
```


- Container(List(ListItem(TodoItem)))
这里我们把TodoItem 数据传给 ListItem, 然后结果作为 List 的参数, 返回的结果又作为 Container的参数

如果用 compose 函数,过程如下

```js
R.compose(Container, List)(<li>Hello World!</li>);

/**
* <div className="panel panel-default">
* <div className="panel-body">
* <ul>
* <li>Hello World!</li>
* </ul>
* </div>
* </div>
*/

const ContainerWithList = R.compose(Container, List);
R.compose(ContainerWithList, ListItem)({id: 123, text: 'Buy milk'});

/**
* <div className="panel panel-default">
* <div className="panel-body">
* <ul>
* <li>
* <span>Buy milk</span>
* </li>
* </ul>
* </div>
* </div>
*/

const TodoItem = {
id: 123,
text: 'Buy milk'
};
const TodoList = R.compose(Container, List, ListItem);
TodoList(TodoItem);

/**
* <div className="panel panel-default">
* <div className="panel-body">
* <ul>
* <li>
* <span>Buy milk</span>
* </li>
* </ul>
* </div>
* </div>
*/

```


- const TodoList = R.compose(Container, List, ListItem)

列表的工厂函数,TodoList 组件可以看作为Container,List和 ListItem 的组合
现在 还只能接受一个参数, 需要可以接受一个数组

```js
const mapTodos = function(todos) {
return todos.map(function(todo) {
return ListItem(todo);
});
};
const TodoList = R.compose(Container, List, mapTodos);
const mock = [
{id: 1, text: 'One'},
{id: 1, text: 'Two'},
{id: 1, text: 'Three'}
];
TodoList(mock);

/**
* <div className="panel panel-default">
* <div className="panel-body">
* <ul>
* <li>
* <span>One</span>
* </li>
* <li>
* <span>Two</span>
* </li>
* <li>
* <span>Three</span>
* </li>
* </ul>
* </div>
* </div>
*/
  • mapTodos 可以有更简单的模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//This
return todos.map(function(todo) {
return ListItem(todo);
});

//Is the same as
return todos.map(ListItem);

//So the result would be
const mapTodos = function(todos) {
return todos.map(ListItem);
};

//The same using Ramda
const mapTodos = function(todos) {
return R.map(ListItem, todos);
};

//Now remember two things from Ramda docs:
// - Ramda functions are automatically curried
// - The parameters to Ramda functions are arranged to make it convenient for currying.
// The data to be operated on is generally supplied last.
//So:
const mapTodos = R.map(ListItem);

//At this point mapTodos variable is rendudant, we don't need it anymore:
const TodoList = R.compose(Container, List, R.map(ListItem));
  • const mapTodos = R.map(ListItem); Ramda 函数式自动柯理化的,所以代码是这样的, 等待传递数据数组,返回的数组的形式是
  • {data.item}
  • 组成的数组

完整的 TodoList 的代码就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'React';
import R from 'ramda';

const Container = children => (<div className="panel panel-default">
<div className="panel-body">
{children}
</div>
</div>);

const List = children => (<ul>
{children}
</ul>);

const ListItem = ({ id, text }) => (<li key={id}>
<span>{text}</span>
</li>);

const TodoList = R.compose(Container, List, R.map(ListItem));

export default TodoList;

工厂配置好了,就等数据了

  • 模拟一下 appState 的 todo 数据
1
2
3
4
5
6
7
8
let appState = {
secondsElapsed: 0,
todos: [
{id: 1, text: 'Buy milk'},
{id: 2, text: 'Go running'},
{id: 3, text: 'Rest'}
]
};
  • 在 App 组件中添加 TodoList 组件作为子组件
1
2
3
4
5
6
import TodoList from './todo-list';
const App = appState => (<div className="container">
<h1>App name</h1>
<Timer secondsElapsed={appState.secondsElapsed} />
<TodoList todos={appState.todos} />
</div>);

TodoList组件期待的参数是一个todos数组,

1
2
<TodoList todos={appState.todos} />
//const TodoList = R.compose(Container, List, R.map(ListItem))

React stateless component是作为函数的,所以我们也可以传递参数

1
TodoList({todos: appState.todos});

最好是传递单个参数,所以这种情况,再改进一下

1
const TodoList = R.compose(Container, List, R.map(ListItem), R.prop('todos'));

调用就直接改为:

1
TodoList(appState)

结束